Skip to content

fix(sqlite): apply each WAL commit in one Ladybug transaction - #142

Merged
adsharma merged 1 commit into
LadybugDB:mainfrom
adsharma:sqlite-tx-per-commit
Nov 9, 2026
Merged

adsharma merged 1 commit into
LadybugDB:mainfrom
adsharma:sqlite-tx-per-commit

Conversation

@adsharma

@adsharma adsharma commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

One SQLite WAL commit now maps to one Ladybug transaction: the batch's statements plus its cursor advance commit atomically.

Motivation. SyncEngine::sync ran every statement through the inner context in autocommit, so a tick with N statements was N transactions. A mid-tick failure left committed prefixes behind — observed live: node upserts for a staged batch committed, then the edge batch failed with Binder exception: Cannot find property _sqlite_rowid on a pre-existing rel table, leaving a half-replicated batch (duplicate memory + entity rows after the fallback applier converged it).

Change (sqlite/src/function/sync_engine.cpp):

  • New applyBatchTx wrapper: BEGIN … batch statements … cursor upsert … COMMIT, with ROLLBACK (and stats-restore) on any statement failure.
  • All four apply sites (bootstrap, WAL-restart reconcile, DDL reconcile, per-commit incremental loop) go through it; the end-of-tick cursor write is removed in favor of the per-batch in-txn advance, so crash recovery resumes from the last committed SQLite commit.
  • DDL (CREATE … IF NOT EXISTS, shadow healing) stays outside the transaction: replay-safe by construction.

Validation.

  • New consumer-side regression tests (ladybug-memory-rs, sqlite_first): against the pre-fix build the failed tick leaves (mem, ent, men) = (1, 1, 0); with this build it leaves (0, 0, 0), and the nodes-only batch commits cleanly (via=extension, cursor advanced).
  • C++ sync_api_test could not be built in this environment (no gtest available to the standalone extension build); the cdc unit tests cover untouched code. Recommend CI run the full extension suite.

Follow-up (not in this PR): edge _sqlite_rowid slot on pre-existing rel tables — tracked separately.

One SQLite WAL commit now maps to one Ladybug transaction: the batch's
statements plus its cursor advance commit atomically. A mid-tick failure
(e.g. a binder error on one edge table) leaves no partial rows behind,
and the next tick replays whole batches idempotently.

- applyBatchTx wraps BEGIN .. batch .. cursor .. COMMIT with ROLLBACK on
  any statement failure; stats only count committed work.
- Cursor advances per batch instead of once at tick end, so crash recovery
  resumes from the last committed SQLite commit.
- DDL stays outside the transaction (replay-safe: IF NOT EXISTS /
  one-shot shadow healing).
@adsharma
adsharma merged commit e853e54 into LadybugDB:main Nov 9, 2026
2 checks passed
@adsharma
adsharma deleted the sqlite-tx-per-commit branch November 9, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant